package jgameconsole;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class JGameConsole {
	private Object sandbox = null;
	
	public JGameConsole(Object sandbox) {
		this.sandbox = sandbox;
	}
	
	private boolean isField(String part) {
		return !part.contains("(");
	}
	
	private boolean isMethod(String part) {
		return part.contains("(");
	}

	private boolean validByte(String val)	{ try { Byte.valueOf(val);		return true; } catch (NumberFormatException e) { return false; } }
	private boolean validShort(String val)	{ try { Short.valueOf(val);		return true; } catch (NumberFormatException e) { return false; } }
	private boolean validInteger(String val){ try { Integer.valueOf(val);	return true; } catch (NumberFormatException e) { return false; } }
	private boolean validLong(String val)	{ try { Integer.valueOf(val);	return true; } catch (NumberFormatException e) { return false; } }
	private boolean validFloat(String val)	{ try { Float.valueOf(val);		return true; } catch (NumberFormatException e) { return false; } }
	private boolean validDouble(String val)	{ try { Double.valueOf(val);	return true; } catch (NumberFormatException e) { return false; } }
	private boolean validBool(String val)	{ return val.contains("true") || val.contains("false"); }
	
	private String[] getMethodParameters(String part) {
		String tmp[] = part.substring(part.indexOf('(') + 1, part.indexOf(')')).split(",");
		
		for (int i = 0; i < tmp.length; i++) {
			tmp[i] = tmp[i].replaceAll(" ", "");
		}
		
		if (tmp.length == 1 && tmp[0].equals("")) {
			return new String[0];
		}
		else {
			return tmp;
		}
	}
	
	private boolean isConvertableTo(String possibleParameter, Class<?> definedParameter) {
		if (definedParameter.equals(Byte.class) || definedParameter.equals(byte.class))			return this.validByte(possibleParameter);
		if (definedParameter.equals(Short.class) || definedParameter.equals(short.class))		return this.validShort(possibleParameter);
		if (definedParameter.equals(Integer.class) || definedParameter.equals(int.class))		return this.validInteger(possibleParameter);
		if (definedParameter.equals(Long.class) || definedParameter.equals(long.class))			return this.validLong(possibleParameter);
		if (definedParameter.equals(Float.class) || definedParameter.equals(float.class))		return this.validFloat(possibleParameter);
		if (definedParameter.equals(Double.class) || definedParameter.equals(double.class))		return this.validDouble(possibleParameter);
		if (definedParameter.equals(Boolean.class) || definedParameter.equals(boolean.class))	return this.validBool(possibleParameter);
		
		return false;
	}
	
	private Object convertTo(String value, Class<?> definedParameter) {
		if (definedParameter.equals(Byte.class) || definedParameter.equals(byte.class))			return Byte.valueOf(value); 
		if (definedParameter.equals(Short.class) || definedParameter.equals(short.class))		return Short.valueOf(value); 
		if (definedParameter.equals(Integer.class) || definedParameter.equals(int.class))		return Integer.valueOf(value);
		if (definedParameter.equals(Long.class) || definedParameter.equals(long.class))			return Long.valueOf(value);
		if (definedParameter.equals(Float.class) || definedParameter.equals(float.class))		return Float.valueOf(value);
		if (definedParameter.equals(Double.class) || definedParameter.equals(double.class))		return Double.valueOf(value);
		if (definedParameter.equals(Boolean.class) || definedParameter.equals(boolean.class))	return Boolean.valueOf(value);
		
		return null;
	}
	
	private String parseMethodName(String part) {
		return part.substring(0, part.indexOf('('));
	}
	
	private Object getSubResult(Object overObject, String part) throws InvalidCommand {
		Class<?> clazz = overObject.getClass();
		
		if (this.isField(part)) {
			try {
				Field field = clazz.getDeclaredField(part);
				
				field.setAccessible(true);
				return field.get(overObject);
			} catch (Exception e) {
			}
		}
		else
		if (this.isMethod(part)) {
			try {
				Method[]	methods		= clazz.getDeclaredMethods();
				String[]	parameters	= this.getMethodParameters(part);
				
				Method		method		= null;
				Class<?>[]	toTypes		= null;
				
				for (Method tmpMethod : methods) {
					if (tmpMethod.getName().equals(parseMethodName(part))) {
						Class<?>[] definedParameters = tmpMethod.getParameterTypes();
						
						if (parameters.length != definedParameters.length) {
							continue;
						}
						
						method	= tmpMethod;
						toTypes = definedParameters;
						
						for (int i = 0; i < definedParameters.length; i++) {
							if (!isConvertableTo(parameters[i], definedParameters[i])) {
								method = null;
							}
						}
					}
					
					if (method != null) {
						break;
					}
				}

				if (method != null) {
					// TODO: Horrible! "Invoke" takes a variable number of arguments. Is there a way to replace it with single call? 
					if (parameters.length == 0) return method.invoke(overObject);
					if (parameters.length == 1) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]));
					if (parameters.length == 2) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]));
					if (parameters.length == 3) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]));
					if (parameters.length == 4) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]), convertTo(parameters[3], toTypes[3]));
					if (parameters.length == 5) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]), convertTo(parameters[3], toTypes[3]), convertTo(parameters[4], toTypes[4]));
					if (parameters.length == 6) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]), convertTo(parameters[3], toTypes[3]), convertTo(parameters[4], toTypes[4]), convertTo(parameters[5], toTypes[5]));
					if (parameters.length == 7) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]), convertTo(parameters[3], toTypes[3]), convertTo(parameters[4], toTypes[4]), convertTo(parameters[5], toTypes[5]), convertTo(parameters[6], toTypes[6]));
					if (parameters.length == 8) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]), convertTo(parameters[3], toTypes[3]), convertTo(parameters[4], toTypes[4]), convertTo(parameters[5], toTypes[5]), convertTo(parameters[6], toTypes[6]), convertTo(parameters[7], toTypes[7]));
					if (parameters.length == 9) return method.invoke(overObject, convertTo(parameters[0], toTypes[0]), convertTo(parameters[1], toTypes[1]), convertTo(parameters[2], toTypes[2]), convertTo(parameters[3], toTypes[3]), convertTo(parameters[4], toTypes[4]), convertTo(parameters[5], toTypes[5]), convertTo(parameters[6], toTypes[6]), convertTo(parameters[7], toTypes[7]), convertTo(parameters[8], toTypes[8]));
				}
			} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			}
		}
		
		throw new InvalidCommand();
	}
	
	private String[] getParts(String command) {
		ArrayList<String> result = new ArrayList<String>();
		result.add("");
		
		int tmp = 0;
		boolean insideParams = false;
		
		for (int i = 0; i < command.length(); i++) {
			if (command.charAt(i) == '(') {
				insideParams = true;
			}
			else
			if (command.charAt(i) == ')') {
				insideParams = false;
			}
			else
			if (!insideParams) {
				if (command.charAt(i) == '.') {
					tmp++;
					result.add("");
					continue;
				}
			}
			
			result.set(tmp, result.get(tmp) + command.charAt(i));
		}
		
		String[] arrResult = new String[result.size()];
		for (int i = 0; i < result.size(); i++) {
			arrResult[i] = result.get(i);
		}
		
		return arrResult;
	}
	
	public String processCommand(String command) throws InvalidCommand {
		String[]	parts		= this.getParts(command);
		Object		overObject	= this.sandbox;
		
		for (int i = 0; i < parts.length; i++) {
			overObject = this.getSubResult(overObject, parts[i]);
		}
		
		return (overObject != null) ? overObject.toString() : "null";
	}
}
